#!/usr/local/bin/perl -w
use strict;
#sub POE::Kernel::ASSERT_DEFAULT () {1}
#sub POE::Kernel::TRACE_DEFAULT  () {1}
use FindBin;
use lib "$FindBin::Bin/../POE-Component-Client-AirTunes/lib";
use lib "$FindBin::Bin/../POE-Component-Rendezvous/lib";
use lib "$FindBin::Bin/../POE-Component-Client-DAAP/lib";

use Data::Dumper;

package WebAirTunes;
use base qw( Class::Accessor );
__PACKAGE__->mk_accessors(qw( httpd port tracks shares current_track airtunes ));

use POE qw/Component::Client::AirTunes
           Component::Server::HTTP
           Component::Client::DAAP
           Component::Rendezvous/;

use HTTP::Status;
use HTML::Entities;

our $id = 0;

sub trim {
    my($str, $len) = @_;
    if (length($str) > $len) {
        $str = substr($str, 0, $len);
        return "$str ...";
    }
    return $str;
}

sub encode_html { HTML::Entities::encode_entities($_[0], q(<>"')) }   # "))}

sub new {
    my $class = shift;
    my $self = bless { airtunes => {},
                       shares   => {},
                       tracks   => {},
                       @_ }, $class;
    $self->find_tracks;
    $self->find_airtunes;
    $self->httpd( POE::Component::Server::HTTP->new(
        Port => $self->port,
        ContentHandler => { '/'     => sub { $self->browse(@_) },
                            '/play' => sub { $self->play(@_) },
                            '/stop' => sub { $self->stop(@_) } },
    ));

    *WebAirTunes::Server::server = sub { $self };

    return $self;
}

sub browse {
    my($self, $req, $res) = @_;

    $res->code(RC_OK);
    $res->content_type('text/html; charset=utf-8');
    my $content = <<HTML;
<html>
<head>
<style>
h1 { font-size: 2em }
h2,h3 { font-size: 1.2em }
body,th,td,option { font-family: trbuchet MS; font-size: 12px }
th.song { width: 40% }
th.artist { width: 30% }
th.album { width: 30% }
#airtunes-box { align: right }
</style>
<script>
function doStream(sid, id) {
    location.href = '/play?sid=' + sid + '&id=' + id + '&airtunes=' + document.getElementById('airtunes').value;
}
function doStop() {
    location.href = '/stop?airtunes=' + document.getElementById('airtunes').value;
}
</script>
</head>
<body>
<h1>WebAirTunes</h1>

<div id="current">
Now playing: @{[ $self->current_track ? encode_html( trim($self->current_track->as_now_playing, 128) ) . qq( <a href="javascript:doStop()">[Stop]</a>) : '' ]}
</div>

<form>
<div id="airtunes-box">
<select id="airtunes">
HTML
    ;

    for my $at_sid (keys %{$self->airtunes}) {
        $content .= <<HTML;
<option value="$at_sid">@{[ encode_html($self->airtunes->{$at_sid}->name) ]}</option>
HTML
    ;
    }

    $content .= "</select></div>";

    for my $sid (keys %{ $self->tracks }) {
        $content .=<<HTML;
<h2>@{[ encode_html($self->shares->{$sid}->name) ]}</h2>
<table>
<tr><th class="song">Song</th><th class="artist">Artist</th><th class="album">Album</th></tr>
HTML
    ;

        for my $track (sort { $a->daap_songartist cmp $b->daap_songartist } values %{ $self->tracks->{$sid} }) {
            $content .= <<HTML;
<tr><td><a href="javascript:doStream(@{[ $track->session_id ]},@{[ $track->dmap_itemid ]})">[P]</a> @{[ encode_html(trim($track->dmap_itemname, 48)) ]}</td><td>@{[ encode_html(trim($track->daap_songartist, 32)) ]}</td><td>@{[ encode_html(trim($track->daap_songalbum, 32)) ]}</td></tr>
HTML
    ;
        }
        $content .= "</table>";
    }

    $content .= <<HTML;
</form>
</body></html>
HTML
    ;

    utf8::encode($content);
    $res->content($content);
    return RC_OK;
}

sub play {
    my($self, $req, $res) = @_;
    my %params = $req->uri->query_form;

    my $track = $self->tracks->{$params{sid}}->{$params{id}};
    $poe_kernel->post($params{sid} => get => {
        event   => 'got_data',
        session => 'daapc',
        airtunes => $params{airtunes},
        format  => $track->daap_songformat,
    }, $params{id});

    $self->current_track($track);
    $self->redirect_root($req, $res);
}

sub stop {
    my($self, $req, $res) = @_;
    my %params = $req->uri->query_form;
    warn "stopping $params{airtunes}";
    $poe_kernel->post("atc_$params{airtunes}" => 'raop_stop');
    $self->redirect_root($req, $res);
}

sub redirect_root {
    my($self, $req, $res) = @_;
    $res->code(RC_FOUND);

    my $uri = $req->uri;
    $uri->path('/');
    $uri->query('');
    $res->header('Location' => $uri);

    return RC_FOUND;
}

sub find_airtunes {
    my $self = shift;

    POE::Session->create(
        inline_states => {
            _start     => \&airtunes_start,
            discovered => \&airtunes_discovered,
            entries    => \&airtunes_entries,
        },
        heap => { self => $self },
    );
}

sub airtunes_start {
    $_[KERNEL]->alias_set('airtunes');
    $_[HEAP]->{at_bonjour} = POE::Component::Rendezvous->new(
        debug => 1,
        rendezvous_options => [ 'raop' ],
        alias => 'at_bonjour',
    );
    $_[HEAP]->{at_bonjour}->yield(discover => { event => 'discovered' });
}

sub airtunes_discovered {
    warn "airtunes discoverd\n";
    $_[HEAP]->{at_bonjour}->entries({ event => 'entries', wantarray => 1 });
}

sub airtunes_entries {
    warn "entries: ".Data::Dumper->Dump([$_[ARG0]->{result}])."\n";
    unless ($_[ARG0]->{result}) {
        warn "no AirTunes found\n";
        return;
    }

    for my $airtunes ( @{$_[ARG0]->{result}} ) {
        my $session_id = POE::Session->create(
            inline_states => {
                _start         => \&raop_start,
                raop_play      => \&raop_play,
                raop_connected => \&raop_connected,
                raop_error     => \&raop_error,
                raop_done      => \&raop_done,
                raop_stop      => \&raop_stop,
            },
            heap => { airtunes => $airtunes },
        )->ID;
        warn "AirTunes session $session_id";

        # AirTunes's name looks like 001124E99DC7\@foobar
        my $name = $airtunes->name;
        $name =~ s/.*\\\@//;
        $airtunes->name($name);

        $_[HEAP]->{self}->airtunes->{$session_id} = $airtunes;
    }
}

sub raop_start {
    $_[KERNEL]->alias_set("atc_" . $_[SESSION]->ID);
}

sub raop_play {
    my $song = $_[ARG0];
    my $sid  = $_[SESSION]->ID;
    warn "playing $song from $sid";
    $_[HEAP]->{song} = $song;

    if (! $_[HEAP]->{raop}) {
        $_[HEAP]->{raop} = POE::Component::Client::AirTunes->new(
            host   => $_[HEAP]->{airtunes}->address,
            parent => "atc_$sid",
            events => {
                connected => 'raop_connected',
                error     => 'raop_error',
                done      => 'raop_done',
                disconnected => 'raop_disconnected',
            },
            alias => "raop_$sid",
            debug => 1,
        );
    } else {
        $_[KERNEL]->post("raop_$sid" => stop => ());
        $_[KERNEL]->yield('raop_connected');
    }
}

sub raop_connected {
    my $sid = $_[SESSION]->ID;
    warn "connected $sid";
    $_[KERNEL]->post("raop_$sid" => volume => 100);
    $_[KERNEL]->post("raop_$sid" => play   => $_[HEAP]->{song});
}

sub raop_error {
    warn "raop error: $_[ARG0]";
    delete $_[HEAP]->{raop};
}

sub raop_disconnected {
    delete $_[HEAP]->{raop};
}

sub raop_stop {
    my $sid = $_[SESSION]->ID;
    $_[KERNEL]->post("raop_$sid" => stop => ());
    WebAirTunes::Server->server->current_track( undef );
}

sub raop_done {
    warn "playing $_[HEAP]->{song} done\n";
    WebAirTunes::Server->server->current_track( undef );
}

sub find_tracks {
    my $self = shift;

    POE::Session->create(
        inline_states => {
            _start     => \&daap_start,
            connected  => \&daap_connected,
            dbs        => \&daap_dbs,
            songs      => \&daap_songs,
            got_data   => \&daap_got_data,
            test       => \&test,
            add_track  => sub { $self->add_track(@_[ARG0..$#_]) },

            published  => \&published,
            discovered => \&discovered,
            entries    => \&entries,
        },
        heap => {
            self => $self,
        },
    );
}

sub daap_start {
    $_[KERNEL]->alias_set('daapc');
    warn "our session is ".$_[SESSION]->ID."\n";

    $_[HEAP]->{bonjour} = POE::Component::Rendezvous->new(
        debug => 1,
        rendezvous_options => [
            "daap"
        ],
        alias => 'bonjour',
    );
    $_[HEAP]->{bonjour}->yield(discover => { event => 'discovered' });
}

sub discovered {
    warn "discovered\n";
    $_[HEAP]->{bonjour}->entries({ event => 'entries', wantarray => 1 });
}

sub entries {
#    warn "entries: ".Data::Dumper->Dump([$_[ARG0]->{result}])."\n";
    unless ($_[ARG0]->{result}) {
        warn "no itunes servers found\n";
        return;
    }

    for my $share ( @{$_[ARG0]->{result}} ) {
        if ($share->name) {
            my $name = $share->name;
               $name =~ s/\\(\d{3})/chr($1)/eg; # DNS escaped text
            $share->name($name);
            #        $self->name($name . " (DAAP Proxy)");
        }
        $_[HEAP]->{host} = $share->address;
        $_[HEAP]->{port} = $share->port;
        $_[HEAP]->{name} = $share->name;

        my $alias = join ":", "server", $share->address, $share->port;
        my $daap = POE::Component::Client::DAAP->new(
#            alt_fork => 1,
            debug => 1,
            daap_options => [
                SERVER_HOST => $_[HEAP]->{host},
                SERVER_PORT => $_[HEAP]->{port},
                #            PASSWORD => $password
            ],
            alias => $alias,
        );

        my $session_id = $daap->session_id();
        $_[HEAP]->{"server_$session_id"} = $daap;
        $_[HEAP]->{self}->shares->{$session_id} = $share;

        $_[KERNEL]->post($session_id, connect => { event => 'connected', session_id => $session_id });

        warn "finding tracks from ", $share->name, "\n";
    }
}

sub daap_connected {
    unless ($_[ARG0]->{result}) {
        warn "connect failed\n";
        $_[KERNEL]->post($_[ARG0]->{session_id}, 'shutdown');
        return;
    }

    $_[KERNEL]->post($_[ARG0]->{session_id}, databases => { event => 'dbs', session_id => $_[ARG0]->{session_id} });
    warn "getting databases\n";
}

sub daap_dbs {
    warn "databases: ".Data::Dumper->Dump([$_[ARG0]->{result}])."\n";
    $_[KERNEL]->post($_[ARG0]->{session_id}, songs => { event => 'songs', session_id => $_[ARG0]->{session_id} });
    warn "getting songs\n";
}

sub daap_songs {
#    warn "songs: ".Data::Dumper->Dump([$_[ARG0]->{result}]) . "\n";

    for my $id (keys %{$_[ARG0]->{result}}) {
        warn "adding $id from $_[ARG0]->{session_id}";
        $_[KERNEL]->yield(add_track => $_[ARG0]->{session_id} => $_[ARG0]->{result}->{$id});
    }
}

sub daap_got_data {
    my $data = $_[ARG0]->{result};
    warn "data size ", length($data), " bytes\n";

    my $ext = $_[ARG0]->{format} || 'mp3';
    my $tmpfile = "/tmp/web-airtunes-" . $id++ . ".$ext";
    open my $out, ">", $tmpfile or die $!;
    print $out $data;
    close $out;

    $poe_kernel->post("atc_$_[ARG0]->{airtunes}" => raop_play => $tmpfile);
}

sub add_track {
    my($self, $sid, $data) = @_;
    my $track = WebAirTunes::Track->new_from_daap( $sid, $data ) or next;
    $self->tracks->{$sid}->{$track->dmap_itemid} = $track;
}

package WebAirTunes::Track;
use base qw( Net::DAAP::Server::Track );

__PACKAGE__->mk_accessors(qw(daap_songcontentrating com_apple_itunes_itms_storefrontid session_id));

sub new_from_daap {
    my($class, $sid, $song) = @_;

    my $self = $class->SUPER::new;
    for my $field (keys %$song) {
        utf8::decode($song->{$field});
        (my $method = $field) =~  s{[.-]}{_}g;
        $self->$method($song->{$field}) if $self->can($method);
        warn "$method" unless $self->can($method);
    }

    $self->session_id($sid);
    $self;
}

sub as_now_playing {
    my $self = shift;
    my $track = '';
    $track .= $self->daap_songartist . " - " if $self->daap_songartist;
    $track .= $self->dmap_itemname;
    $track;
}

package main;
use Getopt::Long;
use POE;

my $port = 8080;
GetOptions("--port=i", \$port);

my $server = WebAirTunes->new(
    port => $port,
);
$poe_kernel->run;
